import {Callout, Tabs} from 'nextra/components';
import { Widget } from '@/components/demo/widget.tsx';
import LinkBadge from '@/components/ui/link-badge/link-badge.tsx';
import LinkBadgeGroup from '@/components/ui/link-badge/link-badge-group.tsx';

# Select

A select displays a list of drop-down options for the user to pick from.

It is a form-field and can therefore be used in a form.

<LinkBadgeGroup>
    <LinkBadge label="API Reference" href="https://pub.dev/documentation/forui/latest/forui.widgets.select/"/>
</LinkBadgeGroup>

<Callout type="info">
    For multi selections, consider using a [multi select](/docs/form/multi-select).

    For touch devices, a [select tile group](/docs/tile/select-tile-group) or
    [select menu tile](/docs/tile/select-menu-tile) is generally recommended over this.

</Callout>

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='select' query={{}} height={400}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart copy
    const fruits = [
      'Apple',
      'Banana',
      'Blueberry',
      'Grapes',
      'Lemon',
      'Mango',
      'Kiwi',
      'Orange',
      'Peach',
      'Pear',
      'Pineapple',
      'Plum',
      'Raspberry',
      'Strawberry',
      'Watermelon',
    ];

    class SelectPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) => FSelect<String>.rich(
          hint: 'Select a fruit',
          format: (s) => s,
          children: [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
        );
    }
    ```

  </Tabs.Tab>
</Tabs>

## CLI

To generate and customize this style:

```shell copy
dart run forui style create select
```

## Usage

### `FSelect(...)`

```dart copy
FSelect<Locale>(
  items: {
    'United States': Locale('en', 'US'),
    'Canada': Locale('en', 'CA'),
    'Japan': Locale('ja', 'JP'),
  },
  controller: FSelectController<Locale>(vsync: this),
  style: FSelectStyle.inherit(...),
  label: const Text('Country'),
  description: const Text('Select your country of residence'),
  hint: 'Choose a country',
  format: (value) => value.toUpperCase(),
  onChange: (value) => print('Selected country: $value'),
  onSaved: (value) => print('Saved country: $value'),
  onReset: () => print('Reset'),
  autovalidateMode: AutovalidateMode.onUserInteraction,
  builder: (context, style, state, child) => child,
  prefixBuilder: (context, style, states) => Icon(FIcons.globe),
  suffixBuilder: (context, style, states) => Icon(FIcons.arrowDown),
  popoverConstraints: const FAutoWidthPortalConstraints(maxHeight: 400),
  clearable: true,
  contentScrollHandles: true,
);
```

### `FSelect.rich(...)`

```dart copy
FSelect<String>.rich(
  controller: FSelectController<String>(vsync: this),
  style: FSelectStyle(...),
  label: const Text('Country'),
  description: const Text('Select your country of residence'),
  hint: 'Choose a country',
  format: (value) => value.toUpperCase(),
  onChange: (value) => print('Selected country: $value'),
  onSaved: (value) => print('Saved country: $value'),
  onReset: () => print('Reset'),
  autovalidateMode: AutovalidateMode.onUserInteraction,
  builder: (context, style, state, child) => child,
  prefixBuilder: (context, style, states) => Icon(FIcons.globe),
  suffixBuilder: (context, style, states) => Icon(FIcons.arrowDown),
  popoverConstraints: const FAutoWidthPortalConstraints(maxHeight: 400),
  clearable: true,
  contentDivider: FItemDivider.none,
  contentScrollHandles: true,
  initialValue: 'ca',
  children: [
    FSelectSection(
      label: const Text('North American Countries'),
      divider: FItemDivider.none,
      children: [
        FSelectItem(
          title: const Text('United States'),
          divider: FItemDivider.none,
          value: 'us',
        ),
        FSelectItem(
          title: Text('Canada'),
          value: 'ca',
        ),
      ],
    ),
    FSelectItem(
      title: Text('Japan'),
      value: 'jp',
    ),
  ],
);
```

### `FSelect.search(...)`

```dart copy
FSelect<User>.search(
  items: {
    'Bob Ross': User(firstName: 'Bob', lastName: 'Ross'),
    'John Doe': User(firstName: 'John', lastName: 'Doe'),
    'Mary Jane': User(firstName: 'Mary', lastName: 'Jane'),
    'Peter Parker': User(firstName: 'Peter', lastName: 'Parker'),
  },
  controller: FSelectController<User>(),
  style: FSelectStyle.inherit(...),
  label: const Text('User'),
  description: const Text('Search and select a user'),
  builder: (context, styles, child) => child!,
  format: (user) => '${user.firstName} ${user.lastName}',
  hint: 'Search users...',
  popoverConstraints: const FAutoWidthPortalConstraints(maxHeight: 400),
  clearable: true,
  autoHide: false,
  onChange: (value) => print('Selected country: $value'),
  initialValue: 'value',
  contentDivider: FItemDivider.none,
  contentScrollHandles: false,
  contentPhysics: const BouncingScrollPhysics(),
  contentEmptyBuilder: (context, style) => Text('No results'),
  filter: (query) async {
    // Fetch users based on search query
    return fetchUsers(query);
  },
  contentBuilder: (context, style, users) => [
    for (final user in users)
      FSelectItem(title: Text('${user.firstName} ${user.lastName}'), value: user),
  ],
  searchFieldProperties: FSelectSearchFieldProperties(
    autofocus: true,
    hint: 'Type to search...',
  ),
  contentLoadingBuilder: (context, style) => Text('Loading...'),
  contentErrorBuilder: (context, error, stackTrace) => Text('Error...'),
);
```

### `FSelect.searchBuilder(...)`

```dart copy
FSelect<User>.searchBuilder(
  controller: FSelectController<User>(),
  style: FSelectStyle.inherit(...),
  label: const Text('User'),
  description: const Text('Search and select a user'),
  builder: (context, styles, child) => child!,
  format: (user) => '${user.firstName} ${user.lastName}',
  hint: 'Search users...',
  popoverConstraints: const FAutoWidthPortalConstraints(maxHeight: 400),
  clearable: true,
  autoHide: false,
  onChange: (value) => print('Selected country: $value'),
  initialValue: 'value',
  contentDivider: FItemDivider.none,
  contentScrollHandles: false,
  contentPhysics: const BouncingScrollPhysics(),
  contentEmptyBuilder: (context, style) => Text('No results'),
  filter: (query) async {
    // Fetch users based on search query
    return fetchUsers(query);
  },
  contentBuilder: (context, users) => [
    for (final user in users)
      FSelectItem(title: Text('${user.firstName} ${user.lastName}'), value: user),
  ],
  searchFieldProperties: FSelectSearchFieldProperties(
    autofocus: true,
    hint: 'Type to search...',
  ),
  contentLoadingBuilder: (context, style) => Text('Loading...'),
  contentErrorBuilder: (context, error, stackTrace) => Text('Error...'),
);
```

## Examples

### Detailed

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='select' variant='detailed' query={{}} height={400}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart copy
    class DetailedSelectPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) => FSelect<String>(
        hint: 'Type',
        format: (s) => s,
        children: [
          FSelectItem.rich(
            prefix: const Icon(FIcons.bug),
            title: const Text('Bug'),
            subtitle: const Text('An unexpected problem or behavior'),
            value: 'Bug',
          ),
          FSelectItem.rich(
            prefix: const Icon(FIcons.filePlus2),
            title: const Text('Feature'),
            subtitle: const Text('A new feature or enhancement'),
            value: 'Feature',
          ),
          FSelectItem.rich(
            prefix: const Icon(FIcons.messageCircleQuestionMark),
            title: const Text('Question'),
            subtitle: const Text('A question or clarification'),
            value: 'Question',
          ),
        ],
      );
    }
    ```
  </Tabs.Tab>
</Tabs>

### Sections

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='select' variant='section' query={{}} height={400}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart copy
    class SectionSelectPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) => FSelect<String>.rich(
        hint: 'Select a timezone',
        format: (s) => s,
        children: [
          FSelectSection(
            label: const Text('North America'),
            items: {
              for (final item in [
                'Eastern Standard Time (EST)',
                'Central Standard Time (CST)',
                'Mountain Standard Time (MST)',
                'Pacific Standard Time (PST)',
                'Alaska Standard Time (AKST)',
                'Hawaii Standard Time (HST)',
              ])
                item: item,
            },
          ),
          FSelectSection(
            label: const Text('South America'),
            items: {
              for (final item in [
                'Argentina Time (ART)',
                'Bolivia Time (BOT)',
                'Brasilia Time (BRT)',
                'Chile Standard Time (CLT)',
              ])
                item: item,
            },
          ),
          FSelectSection(
            label: const Text('Europe & Africa'),
            items: {
              for (final item in [
                'Greenwich Mean Time (GMT)',
                'Central European Time (CET)',
                'Eastern European Time (EET)',
                'Western European Summer Time (WEST)',
                'Central Africa Time (CAT)',
                'Eastern Africa Time (EAT)',
              ])
                item: item,
            },
          ),
          FSelectSection(
            label: const Text('Asia'),
            items: {
              for (final item in [
                'Moscow Time (MSK)',
                'India Standard Time (IST)',
                'China Standard Time (CST)',
                'Japan Standard Time (JST)',
                'Korea Standard Time (KST)',
                'Indonesia Standard Time (IST)',
              ])
                item: item,
            },
          ),
          FSelectSection(
            label: const Text('Australia & Pacific'),
            items: {
              for (final item in [
                'Australian Western Standard Time (AWST)',
                'Australian Central Standard Time (ACST)',
                'Australian Eastern Standard Time (AEST)',
                'New Zealand Standard Time (NZST)',
                'Fiji Time (FJT)',
              ])
                item: item,
            },
          ),
        ],
      );
    }
    ```
  </Tabs.Tab>
</Tabs>

### Dividers

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='select' variant='divider' query={{}} height={400}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart {5, 10} copy
    class DividerSelectPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) => FSelect<String>.rich(
        hint: 'Select a level',
        contentDivider: FItemDivider.none,
        format: (s) => s,
        children: [
          FSelectSection(
            label: const Text('Level 1'),
            divider: FItemDivider.indented,
            items: {
              for (final item in ['A', 'B']) item: '1$item',
            },
          ),
          FSelectSection(
            label: const Text('Level 2'),
            items: {
              for (final item in ['A', 'B']) item: '2$item',
            },
          ),
          FSelectItem(title: Text('Level 3'), value: '3'),
          FSelectItem(title: Text('Level 4'), value: '4'),
        ],
      );
    }
    ```
  </Tabs.Tab>
</Tabs>

### Searchable

#### Sync

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='select' variant='sync' query={{}} height={400}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart copy
    class SyncSelectPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) => FSelect<String>.searchBuilder(
        hint: 'Select a fruit',
        format: (s) => s,
        filter: (query) => query.isEmpty ? fruits : fruits.where((f) => f.toLowerCase().startsWith(query.toLowerCase())),
        contentBuilder: (context, _, fruits) => [for (final fruit in fruits) FSelectItem.text(fruit)],
      );
    }
    ```
  </Tabs.Tab>
</Tabs>

#### Async

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='select' variant='async' query={{}} height={400}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart copy
    class AsyncSelectPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) => FSelect<String>.searchBuilder(
        hint: 'Select a fruit',
        format: (s) => s,
        filter: (query) async {
          await Future.delayed(const Duration(seconds: 1));
          return query.isEmpty ? fruits : fruits.where((fruit) => fruit.toLowerCase().startsWith(query.toLowerCase()));
        },
        contentBuilder: (context, _, fruits) => [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
      );
    }
    ```
  </Tabs.Tab>
</Tabs>

#### Async with Custom Loading

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='select' variant='async-loading' query={{}} height={400}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart {10-13} copy
    class AsyncLoadingSelectPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) => FSelect<String>.searchBuilder(
        hint: 'Select a fruit',
        format: (s) => s,
        filter: (query) async {
          await Future.delayed(const Duration(seconds: 1));
          return query.isEmpty ? fruits : fruits.where((fruit) => fruit.toLowerCase().startsWith(query.toLowerCase()));
        },
        contentLoadingBuilder: (context, style) => Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text('Here be dragons...', style: style.textFieldStyle.contentTextStyle.maybeResolve({})),
        ),
        contentBuilder: (context, _, fruits) => [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
      );
    }
    ```
  </Tabs.Tab>
</Tabs>

#### Async with Custom Error Handling

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='select' variant='async-error' query={{}} height={400}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart {11-17} copy
    class AsyncErrorSelectPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) => FSelect<String>.searchBuilder(
        hint: 'Select a fruit',
        format: (s) => s,
        filter: (query) async {
          await Future.delayed(const Duration(seconds: 1));
          throw StateError('Error loading data');
        },
        contentBuilder: (context, _, fruits) => [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
        contentErrorBuilder: (context, error, trace) {
          final style = context.theme.selectStyle.iconStyle;
          return Padding(
            padding: const EdgeInsets.all(8.0),
            child: Icon(FIcons.messageCircleX, size: style.size, color: style.color),
          );
        },
      );
    }
    ```
  </Tabs.Tab>
</Tabs>

### Behavior

#### Toggleable Items

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='select' variant='toggleable' query={{}} height={400}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart {7} copy
    class ToggleableSelectPage extends StatefulWidget {
      @override
      State<ToggleableSelectPage> createState() => ToggleableSelectPageState();
    }

    class ToggleableSelectPageState extends State<ToggleableSelectPage> {
      late final _controller = FSelectController(vsync: this, value: 'Apple', toggleable: true);

      @override
      Widget build(BuildContext context) => FSelect<String>.rich(
        hint: 'Select a fruit',
        format: (s) => s,
        controller: _controller,
        children: [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
      );

      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    }
    ```

  </Tabs.Tab>
</Tabs>

#### Clearable

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='select' variant='clearable' query={{}} height={400}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart {6} copy
    class ClearableSelectPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) => FSelect<String>.rich(
        hint: 'Select a fruit',
        format: (s) => s,
        clearable: true, 
        children: [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
      );
    }
    ```
  </Tabs.Tab>
</Tabs>

#### Custom Formatting

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='select' variant='format' query={{}} height={400}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart {13} copy
    class FormatSelectPage extends StatelessWidget {
      static const users = [
        (firstName: 'Bob', lastName: 'Ross'),
        (firstName: 'John', lastName: 'Doe'),
        (firstName: 'Mary', lastName: 'Jane'),
        (firstName: 'Peter', lastName: 'Parker'),
      ];

      @override
      Widget build(BuildContext context) => FSelect<({String firstName, String lastName})>.rich(
        hint: 'Select a user',
        format: (s) => s,
        format: (user) => '${user.firstName} ${user.lastName}',
        children: [for (final user in users) FSelectItem(title: Text(user.firstName), value: user)],
      );
    }
    ```

  </Tabs.Tab>
</Tabs>

#### With Scroll Handles

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='select' variant='scroll-handles' query={{}} height={400}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart {6} copy
    class ScrollHandlesSelectPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) => FSelect<String>.rich(
        hint: 'Select a fruit',
        format: (s) => s,
        contentScrollHandles: true,
        children: [for (final fruit in fruits) FSelectItem(title: Text(fruit), value: fruit)],
      );
    }
    ```
  </Tabs.Tab>
</Tabs>

### Form

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='select' variant='form' query={{}} height={400}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart copy
    class FormSelectPage extends StatefulWidget {

      @override
      State<FormSelectPage> createState() => _FormSelectPageState();
    }

    class _FormSelectPageState extends State<FormSelectPage> with SingleTickerProviderStateMixin {
      static const _departments = ['Engineering', 'Marketing', 'Sales', 'Human Resources', 'Finance'];

      final _formKey = GlobalKey<FormState>();
      late final _departmentController = FSelectController<String>(vsync: this);

      @override
      Widget build(BuildContext context) => Padding(
        padding: const EdgeInsets.all(30.0),
        child: Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              FSelect<String>.rich(
                controller: _departmentController,
                label: const Text('Department'),
                description: const Text('Choose your dream department'),
                format: (s) => s,
                validator: _validateDepartment,
                children: [for (final department in _departments) FSelectItem(title: Text(department), value: department)],
              ),
              const SizedBox(height: 25),
              FButton(
                child: const Text('Submit'),
                onPress: () {
                  if (_formKey.currentState!.validate()) {
                    // Form is valid, do something with department.
                  }
                },
              ),
            ],
          ),
        ),
      );

      String? _validateDepartment(String? department) {
        if (department == null) {
          return 'Please select a department';
        }
        return null;
      }

      @override
      void dispose() {
        _departmentController.dispose();
        super.dispose();
      }
    }
    ```

  </Tabs.Tab>
</Tabs>
